/*
 * Copyright (c) 2020-2099 the original author or authors. All rights reserve.
 */
package org.tianyun.cloud.web.logger;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import org.tianyun.cloud.context.ServletContext;
import org.tianyun.cloud.context.ServletContext.ServletContextHolder;
import org.tianyun.cloud.ext.thread.ThreadPool;
import org.tianyun.cloud.ext.thread.ThreadPoolFactory;
import org.tianyun.cloud.utils.JsonUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.Closeable;
import java.util.Enumeration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor.DiscardPolicy;
import java.util.concurrent.TimeUnit;

/**
 * 输出请求信息
 *
 * @auther ebert_chan
 */
public class LogPrinter implements Closeable {

    /**
     * LOGGER
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(LogPrinter.class);

    /**
     * 忽略日志打印的请求URI
     */
    private static final String[] IGNORES = new String[]{"/error"};

    /**
     * 线程池对象
     */
    private ExecutorService LOG_THREAD_POOL;

    public LogPrinter() {
        int corePoolSize = Runtime.getRuntime().availableProcessors();
        int maximumPoolSize = corePoolSize * 2;
        long keepAliveTime = 10L;
        String threadNamePrefix = "log-pool-";
        LOG_THREAD_POOL = new ThreadPool(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(corePoolSize * 20),
                new ThreadPoolFactory(threadNamePrefix), new DiscardPolicy());
    }

    @Override
    public void close() {
        LOG_THREAD_POOL.shutdown();
    }

    /**
     * 打印请求参数
     *
     * @param request
     * @auther ebert_chan
     */
    public void writeRequestInformation(HttpServletRequest request) {
        // 放入ServletContext，以备后续使用
        Log log = new Log();
        log.setRequestURI(request.getRequestURI());
        log.setTimestamp(System.currentTimeMillis());
        ServletContextHolder.setExtProperty(ServletContext.EXT_LOG, log);

        if (isIgnore(log)) {
            return;
        }

        LOGGER.info("Request URI: {}", log.getRequestURI());

        Enumeration<String> headerNames = request.getHeaderNames();
        if (headerNames != null) {
            StringBuilder sb = new StringBuilder();
            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                Enumeration<String> values = request.getHeaders(name);
                while (values.hasMoreElements()) {
                    sb.append("\r\n\t");
                    sb.append(name).append(": ");
                    sb.append(StringUtils.defaultString(values.nextElement()));
                }
            }
            LOGGER.info("Headers: {}", sb);
        }
    }

    /**
     * 打印输入参数
     *
     * @param args 参数数组
     * @auther ebert_chan
     */
    public void writeMethodInParameters(Object[] args) {
        Log log = this.getLog();
        if (isIgnore(log)) {
            return;
        }
        StringBuilder sb = new StringBuilder();
        if (args != null) {
            for (int i = 0; i < args.length; i++) {
                Object param = args[i];
                if (param != null && (param instanceof ServletRequest || param instanceof ServletResponse || param instanceof HttpSession || param instanceof MultipartFile)) {
                    continue;
                }
                if (sb.length() > 0) {
                    sb.append(", ");
                }
                if (param == null) {
                    sb.append(param);
                    continue;
                }
                if (param instanceof String || param instanceof Character) {
                    sb.append(param);
                } else if (JsonUtils.canSerialize(param.getClass())) {
                    sb.append(JsonUtils.toJsonString(param));
                } else {
                    sb.append(param);
                }
            }
        }
        LOGGER.info("In Parameters: {}", sb);
    }

    /**
     * 打印输出参数
     *
     * @param result
     * @auther ebert_chan
     */
    public void writeMethodOutParameters(Object result) {
        Log log = this.getLog();
        if (isIgnore(log)) {
            return;
        }
        long time = System.currentTimeMillis() - log.getTimestamp();

        if (result == null) {
            LOGGER.info("Out Parameters({}ms): {}", time, "null");
            return;
        }

        Runnable target = () -> {
            StringBuilder sb = new StringBuilder();
            if (result instanceof String || result instanceof Character) {
                sb.append(result);
            } else if (JsonUtils.canSerialize(result.getClass())) {
                sb.append(JsonUtils.toJsonString(result));
            } else {
                sb.append(result);
            }
            LOGGER.info("Out Parameters({}ms): {}", time, sb);
        };
        LOG_THREAD_POOL.submit(target);
    }

    private boolean isIgnore(Log log) {
        if (log == null) {
            return Boolean.TRUE;
        }
        for (String uri : IGNORES) {
            if (uri.equals(log.getRequestURI())) {
                return Boolean.TRUE;
            }
        }
        return Boolean.FALSE;
    }

    public Log getLog() {
        return ServletContextHolder.getValue(ServletContext.EXT_LOG);
    }

    public static class Log {

        private String requestURI;

        private long timestamp;

        public String getRequestURI() {
            return requestURI;
        }

        public void setRequestURI(String requestURI) {
            this.requestURI = requestURI;
        }

        public long getTimestamp() {
            return timestamp;
        }

        public void setTimestamp(long timestamp) {
            this.timestamp = timestamp;
        }

    }

}
